package bancosys.tec.persist.persister.annotation.utils;

import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.commons.beanutils.PropertyUtils;
import org.hibernate.Hibernate;
import org.hibernate.proxy.HibernateProxy;

import bancosys.tec.persist.exception.NaturalKeyNotDefinedException;
import bancosys.tec.persist.persister.annotation.NaturalKey;

/**
 * Classe utilitária para lidar com entidades que possuem chave natural.
 * 
 * @author lundberg
 */
public final class NaturalKeyHelper {

    private static ConcurrentHashMap<Class<?>, PropertyDescriptor> descriptors = new ConcurrentHashMap<Class<?>, PropertyDescriptor>();

    /**
     * Construtor
     */
    private NaturalKeyHelper() {
        // OK
    }

    /**
     * Lê a natural key de uma entidade
     * 
     * @param entity entity
     * @return String
     */
    public static String getNaturalKey(Object entity) {
        if (entity == null) {
            return null;
        }
        
        Object object = unproxy(entity);
        
        try {
            Object value = findNaturalKeyProperty(object.getClass()).getReadMethod().invoke(object);
            return value == null ? null : value.toString();
        } catch (IllegalArgumentException e) {
            throw new IllegalStateException(e);
        } catch (IllegalAccessException e) {
            throw new IllegalStateException(e);
        } catch (InvocationTargetException e) {
            throw new IllegalStateException(e);
        }
    }

    /**
     * Encontra a propriedade anotada com NaturalKey
     * 
     * @param clazz clazz
     * @return PropertyDescriptor
     */
    public static PropertyDescriptor findNaturalKeyProperty(Class<?> clazz) {
        if (!descriptors.containsKey(clazz)) {
            resolveNaturalKeyProperty(clazz);
        }
        return descriptors.get(clazz);
    }

    /**
     * Devolve o valor do parâmetro ignoreCase da NaturalKey. 
     * 
     * @param entity entity 
     * @return true caso a propriedade anotada esteja parametrizada com ignoreCase, falso caso contrário
     */
    public static boolean isNaturalKeyCaseIgnored(Object entity) {
        if (entity == null) {
            return false;
        }
        Object object = unproxy(entity);
        return isNaturalKeyCaseIgnored(object.getClass());
    }
    
    /**
     * Devolve o valor do parâmetro ignoreCase da NaturalKey. 
     * 
     * @param clazz clazz
     * @return true caso a propriedade anotada esteja parametrizada com ignoreCase, falso caso contrário
     */
    public static boolean isNaturalKeyCaseIgnored(Class<?> clazz) {
        Method getter = PropertyUtils.getReadMethod(findNaturalKeyProperty(clazz));
        return getter.getAnnotation(NaturalKey.class).ignoreCase();
    }
    
    /**
     * @param clazz clazz
     */
    private static void resolveNaturalKeyProperty(Class<?> clazz) {
        PropertyDescriptor[] properties = PropertyUtils.getPropertyDescriptors(clazz);
        for (final PropertyDescriptor property : properties) {
            Method getter = PropertyUtils.getReadMethod(property);
            if (getter != null) {
                if (getter.isAnnotationPresent(NaturalKey.class)) {
                    descriptors.putIfAbsent(clazz, property);
                    return;
                }
            }
        }
        throw new NaturalKeyNotDefinedException(clazz);
    }

    /**
     * Devolve o objeto "de verdade", sem estar envolto pelo proxy do Hibernate.
     * 
     * @param target target
     * @return o objeto sem o proxy
     */
    private static Object unproxy(Object target) {
        if (target == null || !(target instanceof HibernateProxy)) {
            return target;
        }
        Hibernate.initialize(target);
        return ((HibernateProxy) target).getHibernateLazyInitializer().getImplementation();
    }
}
